/**
 * 
 */
package Library;

import java.security.InvalidKeyException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Hardeep
 *
 */
//TODO make this into a singleton
public class DeviceManager {
	/**
	 * Thrown when a device with a given name already exists.
	 * @author Hardeep
	 */
	public class DeviceAlreadyExistsExcept extends Exception {
		public DeviceAlreadyExistsExcept() {super();}
		public DeviceAlreadyExistsExcept(String m) {super(m);}
		private static final long serialVersionUID = -7484046179247784244L;
	}
	
	/**
	 * A connection to the local SQL database.
	 */
	public Connection conn;
	
	/**
	 * The folder in which to store all database files.
	 */
	public static String dbFolder = "db/";
	
	/**
	 * The database name.
	 */
	public String dbName = "deviceManager";
	
	/**
	 * Map of all the Devices in the manager, with keys as names and values as
	 * the Devices.
	 */
	private HashMap<String, DeviceInterface> devices = new HashMap<String, DeviceInterface>();
	
	/**
	 * A map of all the registered device types.
	 */
	private static HashMap<String, DeviceInterface> devTypes = new HashMap<String, DeviceInterface>();
	
	/**
	 * A map of all table names, to their creation Strings.
	 */
	private ConcurrentHashMap<String, String> tables = 
		new ConcurrentHashMap<String, String>();
	
	/**
	 * Singleton ftw.
	 */
	public final static DeviceManager INSTANCE = new DeviceManager();
	
	/**
	 * Create a DeviceManager
	 * @throws SQLException thrown on SQL error when creating/loading db/tables.
	 * @throws ClassNotFoundException thrown if the HSQLDB is not found.
	 */
	private DeviceManager() {
		// Load the HSQL Database Engine JDBC driver
        // NOTE :: hsqldb.jar is required and located in /lib.
		// NOTE :: not sure why this is needed, but it's used in example code...
        try {
			Class.forName("org.hsqldb.jdbcDriver");
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		// Create a JDBC connection to the stand-alone hsqldb server.
		try {
			conn = DriverManager.getConnection(
					"jdbc:hsqldb:file:" + dbFolder + dbName,
					"sa", "");
			populateTables();
		} catch (SQLException e) {
			// TODO if this happens we should halt the program
			e.printStackTrace();
		}
	}
	
	/**
	 * Registers a DeviceType with the DeviceManager.
	 * @param type
	 * @param dev
	 */
	public static void registerDeviceType(String type, DeviceInterface dev) {
		devTypes.put(type, dev);
	}
	
	public static HashMap<String, DeviceInterface> getRegisteredDevices() {
		return devTypes;
	}
	
	/**
	 * Creates and adds a new Device of the given type, with the given name.
	 * @param type
	 * @throws DeviceAlreadyExistsExcept 
	 * @throws SQLException 
	 */
	public DeviceInterface newDeviceOfType(String type, String name) 
	throws SQLException, DeviceAlreadyExistsExcept {
		DeviceInterface d = devTypes.get(type).getNewDeviceInstance(name); 
		addDevice(d);
		return d;
	}
	
	/**
	 * Add the given Device to the DeviceManager.
	 * @param d
	 * @throws SQLException thrown on SQL error when creating/loading db/tables.
	 * @throws DeviceAlreadyExistsExcept thrown if a Device already exists with
	 * the given name.
	 */
	public void addDevice(DeviceInterface d) 
	throws SQLException, DeviceAlreadyExistsExcept {
//		Device dev = new Device(name);
		String name = d.getName();
		
		if ( !devices.containsKey(name) ) {
			devices.put(name, d);
			
			// add to SQL
			StringBuilder q = new StringBuilder();
			q.append("INSERT INTO DEVICES(name) VALUES('");
			q.append(name).append("')");
			update(q);
		}
		else
			throw new DeviceAlreadyExistsExcept("Device already exists with given name.");
	}
	
	/**
	 * Return the Device with the specified name.
	 * @param name the name of the Device
	 * @return the Device
	 * @throws InvalidKeyException thrown if the Device with the given name does
	 * not exist.
	 */
	public DeviceInterface getDevice(String name) throws InvalidKeyException {
		if ( devices.containsKey(name) )
			return devices.get(name);
	
		throw new InvalidKeyException("Device with given name does not exist.");
	}
	
	/**
	 * Returns how many Devices are currently in this DeviceManager
	 * @return
	 */
	public int getNumDevices() {
		return devices.size();
	}
	
	/**
	 * Returns a Collection of all the Devices in the DeviceManager
	 * @return
	 */
	public Collection<DeviceInterface> getDevices() {
		return devices.values();
	}
	
	/**
	 * Shut down the backend databse...
	 */
	public synchronized void shutdown() {
		try {
			if ( !conn.isClosed() ) {
				try {
					update("SHUTDOWN"); // +  "COMPACT");
				} catch (SQLException e) {e.printStackTrace();}
				
				conn.close();
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Return true iff the database for the DeviceManager is closed.
	 * @return
	 */
	public boolean isClosed() {
		try {
			return conn.isClosed();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return true;
	}

	/**
	 * Populate all tables in the Database if they do not already exist.
	 * @throws SQLException
	 */
	private void populateTables() throws SQLException {
		// TODO ugh...so need a better way to do this table business
		tables.put("DEVICES", 
				"CREATE CACHED TABLE DEVICES ( " +
					"devID INTEGER IDENTITY, " +
					"name VARCHAR(512))");
		
		tables.put("SYNC_REL", 
				"CREATE CACHED TABLE SYNC_REL ( " +
					"devID INTEGER, " +
					"syncToDevID INTEGER)");
		
		// First, lets get a list of any existing tables	
		String getTables = "SELECT * FROM information_schema.system_tables WHERE TABLE_SCHEM = 'PUBLIC'";
        ResultSet rs = query(getTables);
        
        // Check all TABLE_NAME's in the system_tables, make sure every table
        // name specified in the field tables is present.
        String e = "";
        while ( rs.next() ) {
        	e = rs.getString("TABLE_NAME");
        	if ( tables.containsKey(e) )
        		tables.remove(e);
        }
        
        if ( tables.size() > 0 ) {
	        Enumeration<String> tableEnum = tables.elements();
	        String t = "";
	        while ( tableEnum.hasMoreElements() ) {
	        	t = tableEnum.nextElement();
	        	update(t);
	        }
        }
        rs.close();
        
        // Get any devices that may already exist
        String getDev = "SELECT * FROM DEVICES";
        ResultSet rs2 = query(getDev);
        
        String devName = "";
        Device n = null;
        while ( rs2.next() ) {
        	devName = rs2.getString("name");
        	System.out.println("Found dev["+rs2.getInt("devID")+"]: " + devName);
        	try {
        		n = new Device(devName);
        	} catch (Exception e2) {
        		e2.printStackTrace();
        	}
        	
        	if ( n != null ) {
        		devices.put(devName, n);
        		System.out.println("\tIcon: " + n.getProperty("icon"));
        	}
        }
	}
	
	/**
	 * Run the given update-type expression on the open database.
	 * @param expr the expression to execute
	 * @return the resulting int. 
	 * @throws SQLException
	 */
	private int update(String expr) throws SQLException {
		Statement st = conn.createStatement();
        return st.executeUpdate(expr);
	}
	
	/**
	 * Run the given update-type expression on the open database.
	 * @param expr the expression to execute
	 * @return the resulting int. 
	 * @throws SQLException
	 */
	private int update(StringBuilder expr) throws SQLException {
		return update(expr.toString());
	}
	
	/**
	 * Run the given query and return the ResultSet
	 * @param expression the expression to run
	 * @return a ResultSet
	 * @throws SQLException
	 */
    private synchronized ResultSet query(String expression) 
    throws SQLException {
        Statement st = conn.createStatement();
        return st.executeQuery(expression);
    }
}
